﻿using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Audio;
using System.IO;
using Microsoft.Xna.Framework.Input;
using Fighter.Screens;
using Fighter.ScreenManagers;
using Fighter.Screens.Scenario;

namespace Fighter.GameContent
{
    /// <summary>
    /// A uniform grid of tiles with collections of gems and enemies.
    /// The level owns the player and controls the game's win and lose
    /// conditions as well as scoring.
    /// </summary>
    class Level : IDisposable
    {
        // Physical structure of the level.
        private Tile[,] tiles;
        private Layer[] layers;
        private float cameraPosition;
        private GraphicsDeviceManager graphics;
        // The layer which entities are drawn on top of.
        private const int EntityLayer = 3;
        private int currentInd;

        // Entities in the level.
        public Player Player
        {
            get { return player; }
        }
        Player player;
        
        public Boss Boss
        {
            get { return boss; }
        }
        Boss boss;

        public Teacher Teacher
        {
            get { return teacher; }
        }
        Teacher teacher;

        //private List<Gem> gems = new List<Gem>(); ça pourra être des pièces ou n'importe quoi d'autre
        private List<Enemy> enemies = new List<Enemy>();
        //munitions
        private List<Munition> munitions = new List<Munition>();
        //Variable permettant d'espacer la frequence de tir
        private int intervalMunition = 10;
        //variable disant que l'on tire 1 fois sur 10
        private int currentIntervalMunition = 10;

        //Sword
        private Sword sword;

        // Key locations in the level.
        private Vector2 start;
        private static readonly Point InvalidPosition = new Point(-1, -1);

        // Level game state.
        private Random random = new Random(354668); // Arbitrary, but constant seed

        // Level content.        
        public ContentManager Content
        {
            get { return content; }
        }
        ContentManager content;

        private Game game;
        private PlayerIndex? controllingPlayer;
        private ScreenManager screenManager;

        #region Loading

        /// <summary>
        /// Constructs a new level.
        /// </summary>
        /// <param name="serviceProvider">
        /// The service provider that will be used to construct a ContentManager.
        /// </param>
        /// <param name="fileStream">
        /// A stream containing the tile data.
        /// </param>
        public Level(Game game, Stream fileStream, int levelIndex, GraphicsDeviceManager graphics, PlayerIndex? controllingPlayer, ScreenManager screenManager)
        {
            this.graphics = graphics;
            this.game = game;
            this.controllingPlayer = controllingPlayer;
            this.screenManager = screenManager;
            // Create a new content manager to load content used just by this level.
            content = new ContentManager(game.Services, "Content");
            this.currentInd = levelIndex;
            LoadTiles(fileStream);

            // Load background layer textures. For now, all levels must
            // use the same backgrounds and only use the left-most part of them.
            layers = new Layer[3];
            layers[0] = new Layer(Content, String.Format("Level/Level{0}/Backgrounds/Layer0", levelIndex), 0.3f, (this.Height - 1) * 30);
            layers[1] = new Layer(Content, String.Format("Level/Level{0}/Backgrounds/Layer1", levelIndex), 0.5f, (this.Height - 1) * 30);
            layers[2] = new Layer(Content, String.Format("Level/Level{0}/Backgrounds/Layer2", levelIndex), 1.0f, (this.Height - 1) * 30);
        }

        /// <summary>
        /// Iterates over every tile in the structure file and loads its
        /// appearance and behavior. This method also validates that the
        /// file is well-formed with a player start point, exit, etc.
        /// </summary>
        /// <param name="fileStream">
        /// A stream containing the tile data.
        /// </param>
        private void LoadTiles(Stream fileStream)
        {
            // Load the level and ensure all of the lines are the same length.
            int width;
            List<string> lines = new List<string>();
            using (StreamReader reader = new StreamReader(fileStream))
            {
                string line = reader.ReadLine();
                width = line.Length;
                while (line != null)
                {
                    lines.Add(line);
                    if (line.Length != width)
                        throw new Exception(String.Format("The length of line {0} is different from all preceeding lines.", lines.Count));
                    line = reader.ReadLine();
                }
            }

            // Allocate the tile grid.
            tiles = new Tile[width, lines.Count];

            // Loop over every tile position,
            for (int y = 0; y < Height; ++y)
            {
                for (int x = 0; x < Width; ++x)
                {
                    // to load each tile.
                    char tileType = lines[y][x];
                    tiles[x, y] = LoadTile(tileType, x, y);
                }
            }

            // Verify that the level has a beginning and an end.
            if (Player == null)
                throw new NotSupportedException("A level must have a starting point.");

        }

        /// <summary>
        /// Loads an individual tile's appearance and behavior.
        /// </summary>
        /// <param name="tileType">
        /// The character loaded from the structure file which
        /// indicates what should be loaded.
        /// </param>
        /// <param name="x">
        /// The X location of this tile in tile space.
        /// </param>
        /// <param name="y">
        /// The Y location of this tile in tile space.
        /// </param>
        /// <returns>The loaded tile.</returns>
        private Tile LoadTile(char tileType, int x, int y)
        {
            switch (tileType)
            {
                // Blank space
                case '.':
                    return new Tile(null, TileCollision.Passable);

                // Floating platform
                case 'x':
                    return LoadTile("bloc", TileCollision.Impassable);
                // sol
                case 's':
                    return LoadTile("sol", TileCollision.Impassable);
                // escalier qui monte vers la droite
                case '/':
                    return LoadTile("escaliersVersDroite", TileCollision.Impassable);
                // remplissage du sol
                case 'g':
                    return LoadTile("ground", TileCollision.Impassable);
                // Player 1 start point
                case '+':
                    return LoadStartTile(x, y);
                //next level
                case 'n':
                    return LoadTile("finish", TileCollision.Next);
                //Fin du niveau
                case 'f':
                    return LoadTile("finish", TileCollision.Finish);

                // Various enemies
                case 'A':
                    return LoadEnemyTile(x, y, "MonsterA", 3);
                case 'B':
                    return LoadEnemyTile(x, y, "MonsterB", 5);
                case 'C':
                    return LoadEnemyTile(x, y, "MonsterC", 3);
                case 'D':
                    return LoadEnemyTile(x, y, "MonsterD", 10);
                //Teacher
                case 'T':
                    return LoadTeacherTile(x, y, "Teacher", 50);

                //Void
                case 'd':
                    return LoadTile("ground", TileCollision.Death);

                // Unknown tile type character
                default:
                    throw new NotSupportedException(String.Format("Unsupported tile type character '{0}' at position {1}, {2}.", tileType, x, y));
            }
        }

        /// <summary>
        /// Creates a new tile. The other tile loading methods typically chain to this
        /// method after performing their special logic.
        /// </summary>
        /// <param name="name">
        /// Path to a tile texture relative to the Content/Tiles directory.
        /// </param>
        /// <param name="collision">
        /// The tile collision type for the new tile.
        /// </param>
        /// <returns>The new tile.</returns>
        private Tile LoadTile(string name, TileCollision collision)
        {

            return new Tile(Content.Load<Texture2D>(String.Format("Level/Level{0}/Item/", this.currentInd) + name), collision);
        }


        /// <summary>
        /// Loads a tile with a random appearance.
        /// </summary>
        /// <param name="baseName">
        /// The content name prefix for this group of tile variations. Tile groups are
        /// name LikeThis0.png and LikeThis1.png and LikeThis2.png.
        /// </param>
        /// <param name="variationCount">
        /// The number of variations in this group.
        /// </param>
        private Tile LoadVarietyTile(string baseName, int variationCount, TileCollision collision)
        {
            int index = random.Next(variationCount);
            return LoadTile(baseName + index, collision);
        }


        /// <summary>
        /// Instantiates a player, puts him in the level, and remembers where to put him when he is resurrected.
        /// </summary>
        private Tile LoadStartTile(int x, int y)
        {
            if (Player != null)
                throw new NotSupportedException("A level may only have one starting point.");

            start = RectangleExtensions.GetBottomCenter(GetBounds(x, y));
            player = new Player(this, start, graphics, this.screenManager, this.game, this.controllingPlayer);

            return new Tile(null, TileCollision.Passable);
        }

        /// <summary>
        /// Remembers the location of the level's exit.
        /// </summary>
        private Tile LoadExitTile(int x, int y)
        {
            return LoadTile("Exit", TileCollision.Passable);
        }

        /// <summary>
        /// Instantiates an enemy and puts him in the level.
        /// </summary>
        private Tile LoadEnemyTile(int x, int y, string spriteSet, int life)
        {
            Vector2 position = RectangleExtensions.GetBottomCenter(GetBounds(x, y));
            if (spriteSet.CompareTo("MonsterD") == 0)
                enemies.Add(new Boss(this, position, spriteSet, graphics, life, 300, 300));
            else
                enemies.Add(new Enemy(this, position, spriteSet, graphics, life));
            return new Tile(null, TileCollision.Passable);
        }

        /// <summary>
        /// Instantiates a teacher and puts him in the level.
        /// </summary>
        private Tile LoadTeacherTile(int x, int y, string spriteSet, int life)
        {
            Vector2 position = RectangleExtensions.GetBottomCenter(GetBounds(x, y));
            enemies.Add(new Teacher(this, position, spriteSet, graphics, life, 10000, 10000));
            return new Tile(null, TileCollision.Passable);
        }

        /// <summary>
        /// Instantiates a gem and puts it in the level.
        /// </summary>
        private Tile LoadGemTile(int x, int y)
        {
            Point position = GetBounds(x, y).Center;

            return new Tile(null, TileCollision.Passable);
        }

        /// <summary>
        /// Unloads the level content.
        /// </summary>
        public void Dispose()
        {
            Content.Unload();
        }

        #endregion

        #region Bounds and collision

        /// <summary>
        /// Gets the collision mode of the tile at a particular location.
        /// This method handles tiles outside of the levels boundries by making it
        /// impossible to escape past the left or right edges, but allowing things
        /// to jump beyond the top of the level and fall off the bottom.
        /// </summary>
        public TileCollision GetCollision(int x, int y)
        {
            // Prevent escaping past the level ends.
            if (x < 0 || x >= Width)
                return TileCollision.Impassable;
            // Allow jumping past the level top and falling through the bottom.
            if (y < 0 || y >= Height)
                return TileCollision.Passable;
            if (y > 1 && y < Height)
            {
                if (tiles[x, y - 1].Collision == TileCollision.Finish || tiles[x, y - 2].Collision == TileCollision.Finish)
                {
                    return TileCollision.Finish;
                }
                if (tiles[x, y - 1].Collision == TileCollision.Next || tiles[x, y - 2].Collision == TileCollision.Next)
                {
                    return TileCollision.Next;
                }
            }
            return tiles[x, y].Collision;
        }

        /// <summary>
        /// Gets the bounding rectangle of a tile in world space.
        /// </summary>        
        public Rectangle GetBounds(int x, int y)
        {
            return new Rectangle(x * Tile.Width, y * Tile.Height, Tile.Width, Tile.Height);
        }

        /// <summary>
        /// Gets the specified tile.
        /// </summary>        
        public Texture2D GetTiles(int x, int y)
        {
            return tiles[x, y].Texture;
        }

        /// <summary>
        /// Width of level measured in tiles.
        /// </summary>
        public int Width
        {
            get { return tiles.GetLength(0); }
        }

        /// <summary>
        /// Height of the level measured in tiles.
        /// </summary>
        public int Height
        {
            get { return tiles.GetLength(1); }
        }

        #endregion

        #region Update

        /// <summary>
        /// Updates all objects in the world, performs collision between them,
        /// and handles the time limit with scoring.
        /// </summary>
        public void Update(
            GameTime gameTime,
            KeyboardState keyboardState)
        {
            // Pause while the player is dead or time is expired.
            if (!(Player.Lives > 0))
            {
                // Still want to perform physics on the player.
                Player.ApplyPhysics(gameTime);

            }
            else
            {
                Player.Update(gameTime, keyboardState);
                UpdateEnemies(gameTime);
                UpdateMunitions(gameTime, keyboardState);
                UpdateSword(gameTime, keyboardState);
            }
        }

        /// <summary>
        /// Animates each enemy and allow them to kill the player.
        /// </summary>
        private void UpdateEnemies(GameTime gameTime)
        {
            foreach (Enemy enemy in enemies)
            {
                enemy.Update(gameTime);

                if (Player.BoundingRectangle.Center.X < enemy.BoundingRectangle.Center.X && enemy.IsAgressive)
                {
                    if ((enemy.BoundingRectangle.Center.X - Player.BoundingRectangle.Center.X) < enemy.DistanceBeforeRush)
                    {
                        enemy.Direction = FaceDirection.Left;
                        enemy.ActualMoveSpeed = Enemy.SprintSpeed;
                    }
                    else
                    {
                        if (enemy.ActualMoveSpeed == Enemy.SprintSpeed)
                        {
                            enemy.ActualMoveSpeed = Enemy.MoveSpeed;
                        }
                    }
                }
                else if (Player.BoundingRectangle.Center.X > enemy.BoundingRectangle.Center.X && enemy.IsAgressive)
                {
                    if ((Player.BoundingRectangle.Center.X - enemy.BoundingRectangle.Center.X) < enemy.DistanceBeforeRush)
                    {
                        enemy.Direction = FaceDirection.Right;
                        enemy.ActualMoveSpeed = Enemy.SprintSpeed;
                    }
                    else
                    {
                        if (enemy.ActualMoveSpeed == Enemy.SprintSpeed)
                        {
                            enemy.ActualMoveSpeed = Enemy.MoveSpeed;
                        }
                    }
                }

                // Touching an enemy instantly kills a life of the player
                if (enemy.BoundingRectangle.Intersects(Player.BoundingRectangle))
                {
                    OnPlayerTouch(enemy, gameTime);
                }
            }
        }

        private void OnPlayerTouch(Enemy killedBy, GameTime gameTime)
        {
            if (!(killedBy is Teacher))
            {
                Player.OnTouch(killedBy, gameTime);
            }
            else
            {
                Teacher teacher = (Teacher)killedBy;
                if (teacher.ScenarioDisplayed == false)
                {
                    screenManager.AddScreen(new Scenario1_1Screen(this.game, this.graphics), this.controllingPlayer);
                    teacher.ScenarioDisplayed = true;
                }
            }
        }

        public int GetPlayerLives()
        {
            return player.Lives;
        }

        /// <summary>
        /// Animates each munitions and allow them to touch ennemies.
        /// </summary>
        private void UpdateMunitions(GameTime gameTime, KeyboardState keyboardState)
        {
            //liste de munitions que l'on va vider de la memoire du jeu 
            List<Munition> munitionsToDelete = new List<Munition>();
            //Liste des monstres qui doivent mourrir
            List<Enemy> enemiesToDelete;

            if (keyboardState.IsKeyDown(Config.Instance.Fire))
            {
                if (currentIntervalMunition >= intervalMunition)
                {
                    Vector2 startPositionMunition = player.Position;
                    startPositionMunition.Y -= player.BoundingRectangle.Height / 2;
                    munitions.Add(new Munition(this, startPositionMunition, this.graphics, keyboardState, player.Orientation, this.screenManager, this.game, this.controllingPlayer));
                    currentIntervalMunition = 0;
                }
                else
                {
                    currentIntervalMunition += 1;
                }
            }
            //Il faut que si on lache la touche de tir, notre variable
            //currentIntervalMunition doit etre egale à intervalMunition
            //pour une réactivité exacte au prochain tir
            //Cependant, si on tapote tres vite cette touche, on est capable de depasser la frequence de tir autorisé.
            //Bug a repenser avec un timer par la suite
            if (keyboardState.IsKeyUp(Config.Instance.Fire))
            {
                currentIntervalMunition = 10;
            }

            foreach (Munition munition in munitions)
            {
                munition.Update(gameTime);
                enemiesToDelete = new List<Enemy>();
                foreach (Enemy enemy in enemies)
                {
                    // Touching an enemy instantly kills a life of the player
                    if (munition.BoundingRectangle.Intersects(enemy.BoundingRectangle))
                    {
                        munition.OnTouchEnnemy(enemy, gameTime);
                        munitionsToDelete.Add(munition);
                        if (enemy.Life <= 0)
                        {
                            enemiesToDelete.Add(enemy);
                        }
                        break;
                    }
                }
                //On supprime chaque monstre ayant 0 ou moins de points de vie
                foreach (Enemy enemy in enemiesToDelete)
                {
                    enemies.Remove(enemy);
                }
                if (munition.IsDrawingUpdating == false)
                {
                    munitionsToDelete.Add(munition);
                }
            }
            //On supprime chaque munition qui ont été en contact avec un monstre
            foreach (Munition munition in munitionsToDelete)
            {
                munitions.Remove(munition);
            }
        }

        private void UpdateSword(GameTime gt, KeyboardState ks)
        {
            if (ks.IsKeyDown(Config.Instance.Attack))
            {
                if (sword == null || !sword.state)
                {
                    Vector2 swordVect = new Vector2(player.Position.X + 35, player.Position.Y);
                    sword = new Sword(this, swordVect, this.graphics);
                }
                List<Enemy> killed = new List<Enemy>();
                foreach (Enemy enemy in enemies)
                {
                    // Touching an enemy instantly kills a life of the player
                    if (sword.BoundingRectangle.Intersects(enemy.BoundingRectangle))
                    {
                        sword.OnHitEnemy(enemy, player);
                        if(enemy.Life <= 0)
                        {
                            killed.Add(enemy);
                        }
                    }
                }
                foreach (Enemy k in killed)
                {
                    
                    enemies.Remove(k);
                }
            }
        }


        #endregion

        #region Draw

        /// <summary>
        /// Draw everything in the level from background to foreground.
        /// </summary>
        public void Draw(GameTime gameTime, SpriteBatch spriteBatch, GraphicsDeviceManager graphics)
        {
            ScrollCamera(spriteBatch.GraphicsDevice.Viewport);

            Vector3 screenScalingFactor;

            float horScaling = (float)graphics.GraphicsDevice.PresentationParameters.BackBufferWidth / GameplayScreen.baseScreenSize.X;
            float verScaling = (float)graphics.GraphicsDevice.PresentationParameters.BackBufferHeight / GameplayScreen.baseScreenSize.Y;

            screenScalingFactor = new Vector3(horScaling, verScaling, 1);

            Matrix globalTransformation = Matrix.CreateScale(screenScalingFactor);

            spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default,
                              RasterizerState.CullCounterClockwise, null, globalTransformation);
            for (int i = 0; i < EntityLayer; ++i)
                layers[i].Draw(spriteBatch, cameraPosition);
            spriteBatch.End();


            spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default,
                              RasterizerState.CullCounterClockwise, null, TransformMatrix(graphics));

            ////On dessine les layers
            //for (int i = 0; i < EntityLayer; ++i)
            //    layers[i].Draw(spriteBatch, cameraPosition);

            //On dessine les brics
            DrawTiles(spriteBatch);

            //On dessine les ennemis
            foreach (Enemy enemy in enemies)
            {
                 enemy.Draw(gameTime, spriteBatch);
                 if (enemy is Boss && !(enemy is Teacher))
                     this.boss = (Boss)enemy;
                 if (enemy is Teacher)
                     this.teacher = (Teacher)enemy;
            }
               
                        

            //On dessine le joueur
            Player.Draw(gameTime, spriteBatch);

            //On dessine les munitions
            foreach (Munition munition in munitions)
                munition.Draw(gameTime, spriteBatch);
            //On dessine les munitions lancer par le boss
            if (boss != null)
            {
                foreach (Munition munition in boss.ListMunitions)
                    munition.Draw(gameTime, spriteBatch);
            }
            
            //On dessine les munitions lancées par le teacher
            if (teacher != null)
            {
                foreach (Munition munition in teacher.ListMunitions)
                    munition.Draw(gameTime, spriteBatch);
            }
            


            if (sword != null)
                sword.Draw(gameTime, spriteBatch, Player);


            spriteBatch.End();

            //spriteBatch.Begin();
            //for (int i = EntityLayer + 1; i < layers.Length; ++i)
            //    layers[i].Draw(spriteBatch, cameraPosition);
            //spriteBatch.End();
        }

        /// <summary>
        /// Draws each tile in the level.
        /// </summary>
        private void DrawTiles(SpriteBatch spriteBatch)
        {
            // For each tile position
            for (int y = 0; y < Height; ++y)
            {
                for (int x = 0; x < Width; ++x)
                {
                    // If there is a visible tile in that position
                    Texture2D texture = tiles[x, y].Texture;
                    if (texture != null)
                    {
                        // Draw it in screen space.
                        Vector2 position = new Vector2(x, y) * Tile.Size;
                        spriteBatch.Draw(texture, position, Color.White);
                    }
                }
            }
        }

        private Matrix TransformMatrix(GraphicsDeviceManager graphics)
        {
            Vector3 screenScalingFactor;

            float horScaling = (float)graphics.GraphicsDevice.PresentationParameters.BackBufferWidth / GameplayScreen.baseScreenSize.X;
            float verScaling = (float)graphics.GraphicsDevice.PresentationParameters.BackBufferHeight / GameplayScreen.baseScreenSize.Y;

            screenScalingFactor = new Vector3(horScaling, verScaling, 1);

            Matrix globalTransformation = Matrix.CreateScale(screenScalingFactor);
            Matrix cameraTransform = Matrix.CreateTranslation(-cameraPosition * graphics.GraphicsDevice.PresentationParameters.BackBufferWidth / GameplayScreen.baseScreenSize.X, 0.0f, 0.0f);

            return globalTransformation * cameraTransform;
        }

        private void ScrollCamera(Viewport viewport)
        {
            //Constante égale à 35% de l'écran
            const float ViewMargin = 0.35f;
            // Calculate the edges of the screen.
            float marginWidth = GameplayScreen.baseScreenSize.X * ViewMargin;
            float marginLeft = cameraPosition + marginWidth;
            float marginRight = cameraPosition + GameplayScreen.baseScreenSize.X - marginWidth;

            // Calculate how far to scroll when the player is near the edges of the screen.
            float cameraMovement = 0.0f;
            if (Player.Position.X < marginLeft)
                cameraMovement = Player.Position.X - marginLeft;
            else if (Player.Position.X > marginRight)
                cameraMovement = Player.Position.X - marginRight;

            // Update the camera position, but prevent scrolling off the ends of the level.
            float maxCameraPosition = Tile.Width * Width - GameplayScreen.baseScreenSize.X;
            cameraPosition = MathHelper.Clamp(cameraPosition + cameraMovement, 0.0f, maxCameraPosition);
        }

        #endregion
    }
}